package au.com.finalconcept.roo.addon.behaviours;

import java.lang.reflect.Modifier;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.felix.scr.annotations.Reference;
import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.ItdTypeDetailsBuilder;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.details.MethodMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.project.LogicalPath;
import org.springframework.uaa.client.util.Assert;

/**
 * This type produces metadata for a new ITD. It uses an
 * {@link ItdTypeDetailsBuilder} provided by
 * {@link AbstractItdTypeDetailsProvidingMetadataItem} to register a field in
 * the ITD and a new method.
 * 
 * @since 1.1.0
 */
public class RooTimestampableMetadata extends AbstractItdTypeDetailsProvidingMetadataItem {
    /**
     * Get hold of a JDK Logger
     */
    private Logger log = Logger.getLogger(getClass().getName());

    private static final String PROVIDES_TYPE_STRING = RooTimestampableMetadata.class.getName();
    private static final String PROVIDES_TYPE = MetadataIdentificationUtils.create(PROVIDES_TYPE_STRING);

    @Reference
    private TypeLocationService typeLocationService;

    protected RooTimestampableMetadata(String identifier, JavaType aspectName, PhysicalTypeMetadata governorPhysicalTypeMetadata) {
        super(identifier, aspectName, governorPhysicalTypeMetadata);
        Assert.isTrue(isValid(identifier), "Metadata identification string '" + identifier + "' does not appear to be a valid");

        // Adding a new sample field definition
        builder.addField(getCreatedAtField());
        builder.addField(getUpdatedAtField());

        // Adding a new sample method definition
        builder.addMethod(getCreatedGetterMethod());
        builder.addMethod(getCreatedSetterMethod());
        builder.addMethod(getUpdatedGetterMethod());
        builder.addMethod(getUpdatedSetterMethod());
        builder.addMethod(getPrePersistMethod());
        builder.addMethod(getPreUpdateMethod());

        // Create a representation of the desired output ITD
        itdTypeDetails = builder.build();
    }

    public static final LogicalPath getPath(String metadataIdentificationString) {
        return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, metadataIdentificationString);
    }

    public static boolean isValid(String metadataIdentificationString) {
        return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, metadataIdentificationString);
    }

    public static final JavaType getJavaType(String metadataIdentificationString) {
        return PhysicalTypeIdentifierNamingUtils.getJavaType(PROVIDES_TYPE_STRING, metadataIdentificationString);
    }

    public static final String createIdentifier(JavaType javaType, LogicalPath path) {
        return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path);
    }

    public static final String getMetadataIdentiferType() {
        return PROVIDES_TYPE;
    }

    private MethodMetadata getPrePersistMethod() {
        ArrayList<AnnotationMetadataBuilder> annotations;
        AnnotationMetadataBuilder annotation;
        InvocableMemberBodyBuilder bodyBuilder;
        final JavaSymbolName methodName = new JavaSymbolName("prePersist");
        AnnotationMetadata slugableAnnotation;
        final JavaType prePersistAnnotationType = new JavaType("javax.persistence.PrePersist");

        MethodMetadata method = Util.methodExists(governorTypeDetails, methodName, new ArrayList<AnnotatedJavaType>());
        if (method != null) {
            log
                    .warning("Found method of type prePersist on entity "
                            + governorTypeDetails.getName()
                            + " the setting of the createdAt and updatedAt may not work as expected unless you set them yourself. Please use the perPersistImpl method");
            return method;
        }

        // Are any of the governer methtods annotated with the PrePersist
        List<MethodMetadata> methods;
        methods = governorTypeDetails.getMethods();
        for (MethodMetadata mMetadata : methods) {
            if (mMetadata.getAnnotation(prePersistAnnotationType) != null) {
                String message;

                message = String
                        .format(
                                "Found method named %s on type %s that is annotated with the PrePersist annotation. Setting of Timestampable fields "
                                        + "(createdAt and updatedAt) may not work properly. Please use the %s instead, this method will be called by automaticly for you",
                                    method.getMethodName().getReadableSymbolName(),
                                    governorTypeDetails.getName().getFullyQualifiedTypeName(),
                                    Util.METHOD_NAME_PRE_PERSIST_IMPL);
                log.severe(message);

                return method;
            }
        }

        // Define method annotations (PrePersist in this case)
        annotations = new ArrayList<AnnotationMetadataBuilder>();
        annotation = new AnnotationMetadataBuilder(prePersistAnnotationType);
        annotations.add(annotation);
        // Create the method body
        bodyBuilder = new InvocableMemberBodyBuilder();
        RooTimestampableMetadata.writePrePersistMethodBody(bodyBuilder);

        // Check if the governor is annotated with other Roo Behaviour
        // annotations that
        // require the prePersist
        slugableAnnotation = governorTypeDetails.getAnnotation(new JavaType(RooSlugable.class));
        if (slugableAnnotation != null) {
            log.info(String.format("Type [%s] is annotated with the RooSlugable. Timestampable is designed to be the "
                    + "master of the prePersist/preUpdate. RooSlugable should respect this and not create the methods. "
                    + "Will request the prePersist body from RooSlugableMetadata", governorTypeDetails
                    .getName()
                        .getFullyQualifiedTypeName()));

            RooSlugableMetadata.writePrePersistMethodBody(this.governorTypeDetails, bodyBuilder, log);
        }

        // Use the MethodMetadataBuilder for easy creation of MethodMetadata
        MethodMetadataBuilder methodBuilder;

        methodBuilder = new MethodMetadataBuilder(this.getId(), Modifier.PUBLIC, methodName, JavaType.VOID_PRIMITIVE, bodyBuilder);
        methodBuilder.setAnnotations(annotations);

        // Add the call prePersistImpl if the developer requires it
        if (governorTypeDetails.getMethod(new JavaSymbolName(Util.METHOD_NAME_PRE_PERSIST_IMPL)) != null) {
            bodyBuilder.appendFormalLine("this." + Util.METHOD_NAME_PRE_PERSIST_IMPL + "();");
        }

        return methodBuilder.build(); // Build and return a MethodMetadata
                                      // instance
    }

    public static void writePrePersistMethodBody(InvocableMemberBodyBuilder bodyBuilder) {

        bodyBuilder.appendFormalLine("this.createdAt = new Date();");
        bodyBuilder.appendFormalLine("this.updatedAt = new Date();");

    }

    private MethodMetadata getPreUpdateMethod() {
        final JavaSymbolName methodName = new JavaSymbolName("preUpdate");
        MethodMetadata method;
        MethodMetadataBuilder methodBuilder;

        method = Util.methodExists(governorTypeDetails, methodName, new ArrayList<AnnotatedJavaType>());
        if (method != null) {
            log
                    .warning("Found method of type preUpdate on entity "
                            + governorTypeDetails.getName()
                            + " the setting of the updatedAt may not work as expected unless you set them yourself. Please use the perUpdateImpl method");
            return method;
        }

        methodBuilder = new MethodMetadataBuilder(this.getId());
        methodBuilder.setMethodName(methodName);
        methodBuilder.setReturnType(JavaType.VOID_PRIMITIVE);
        methodBuilder.addAnnotation(new AnnotationMetadataBuilder("javax.persistence.PreUpdate"));
        RooTimestampableMetadata.writePreUpdateMethodBody(methodBuilder.getBodyBuilder());

        // Add the call prePersistImpl if the developer requires it
        if (governorTypeDetails.getMethod(new JavaSymbolName(Util.METHOD_NAME_PRE_UPDATE_IMPL)) != null) {
            methodBuilder.getBodyBuilder().appendFormalLine("this." + Util.METHOD_NAME_PRE_UPDATE_IMPL + "();");
        }
        return methodBuilder.build();
    }

    public static void writePreUpdateMethodBody(InvocableMemberBodyBuilder bodyBuilder) {

        bodyBuilder.appendFormalLine("this.updatedAt = new Date();");

    }

    private FieldMetadata getCreatedAtField() {
        return Util.getDateField(this.getId(), "createdAt", "created_at");
    }

    private FieldMetadata getUpdatedAtField() {
        return Util.getDateField(this.getId(), "updatedAt", "updated_at");
    }

    private MethodMetadata getCreatedGetterMethod() {
        return Util.getGetterMethod(this.governorTypeDetails, Date.class, this.getId(), "getCreatedAt", "createdAt");
    }

    private MethodMetadata getCreatedSetterMethod() {
        return Util.getSetterMethod(this.governorTypeDetails, Date.class, this.getId(), "setCreatedAt", "createdAt");
    }

    private MethodMetadata getUpdatedGetterMethod() {
        return Util.getSetterMethod(this.governorTypeDetails, Date.class, this.getId(), "getUpdatedAt", "updatedAt");
    }

    private MethodMetadata getUpdatedSetterMethod() {
        return Util.getSetterMethod(this.governorTypeDetails, Date.class, this.getId(), "setUpdatedAt", "updatedAt");
    }

    // Typically, no changes are required beyond this point

    public String toString() {
        final ToStringBuilder builder = new ToStringBuilder(this);
        builder.append("identifier", getId());
        builder.append("valid", valid);
        builder.append("aspectName", aspectName);
        builder.append("destinationType", destination);
        builder.append("governor", governorPhysicalTypeMetadata.getId());
        builder.append("itdTypeDetails", itdTypeDetails);
        return builder.toString();
    }
}
